# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# This module contains the FuzzyCheck class that uses the validators
# defined in the validators modules. The validators make
# use of the FuzzyComparator class that allows validations to be made in
# a fuzzy way that leads to a score instead of a true/false decision.
import math
import re

from table import Table

class FuzzyCheck(object):
  """
  This class can validate a list of gestures to a previously defined set of
  validators. This is used to check if the generated gestures actually
  represent the desired behavior.
  To allow more fine-grained results the output is a score instead of a
  match/no-match decision.
  There are two types of validators: expected or unexpected.
  Expected validators describe the expected behavior in the order it should
  happen. Whenever one of these validators cannot handle a gesture it is
  treated as unexpected and checked by the unexpected validators. A common
  case is expecting a ButtonClick, but allowing a certain amount of
  unexpected Motion to happen.
  """
  def __init__(self):
    self.expected = []
    self.unexpected = []

  def Check(self, event_list):
    """
    Validate gestures against the validators of this object. The expected
    and unexpected variables have to be set up before calling this method.
    This method will return a score between 0..1. It might also return False
    which is different from 0. False describes a complete test-failure, i.e.
    something has happened that should not have happened.
    When it comes to usability impact, it is better when nothing happens, as if
    something completely wrong happens. This is why 'nothing' happened is rated
    slightly better (score=0) as a non-matching behavior (score=False).
    """

    # special case for empty event list
    if len(event_list) == 0:
      if len(self.expected) == 0:
        return 1, "success, no events expected"
      else:
        return 0, "failure, expected events but nothing happened"

    # select first validator
    current_validator = None
    validators = list(reversed(self.expected))
    if len(self.expected) > 0:
      current_validator = validators.pop()

    table = Table("    ")
    table.title = "Validation Log"
    table.header("start - end", "event", "validator (* unexpected)")

    def AddRow(event, validator, unexpected=False):
      if unexpected:
         validator = "*" + str(validator)
      table.row("%f - %f" % (event.start, event.end), event, validator)

    def CheckUnexpectedEvent(event):
      for validator in self.unexpected:
        if validator.Accept(event):
          validator.Validate(event)
          AddRow(event, validator, True)
          return True
      # no validator found. Fail.
      AddRow(event, "no matching validator")
      return False

    failed = False
    for event in event_list:
      if failed:
        AddRow(event, "-")
        continue

      if current_validator is None:
        # No validator left. All events unexpected.
        if not CheckUnexpectedEvent(event):
          failed = True
      else:
        if not current_validator.Accept(event):
          if (current_validator.score > 0 and len(validators) > 0
              and validators[-1].Accept(event)):
              # transition to next validator
              current_validator = validators.pop()
              current_validator.Validate(event)
              AddRow(event, current_validator)
          else:
            # unexpected event
            if not CheckUnexpectedEvent(event):
              failed = True
        else:
          # accepted by current validator
          current_validator.Validate(event)
          AddRow(event, current_validator)

    score_table = self._BuildScoreTable(False)
    report = str(table) + "\n" + str(score_table)

    # calculate score
    if failed:
      score = False
    else:
      score = 1

      # expected validators score
      for validator in self.expected:
        if validator.score is False:
          return False, report
        else:
          score = score * validator.score

      # unexpected validators score
      for validator in self.unexpected:
        if validator.score is False:
          return False, report
        else:
          score = score * validator.score

    score_table = self._BuildScoreTable(score)
    report = str(table) + "\n" + str(score_table)

    return score, report

  def _BuildScoreTable(self, overall):
    table = Table("    ")
    table.title = "Score Breakdown"

    table.header("expected validator", "score")
    for validator in self.expected:
      table.row(validator, validator.score)


    table.header("unexpected validator", "score")
    for validator in self.unexpected:
      table.row(validator, validator.score)

    table.footer = "overall score: " + str(overall)
    return table


################################################################################
# Fuzzy Comparator
################################################################################

class FuzzyComparator(object):
  """
  This class is able to calculate fuzzy comparisons to return a scoring
  value. Fuzzy comparisons are inequality/equality operators with
  an addition range of validity. The result of the comparison is not
  a true/false decision but a score based on the range of validity.
  The comparison string format is:
    "[op] value [ ~ range ]"
  With the following options for op: ==, >, >=, <, <=
  Value being the integer value we Compare to.
  The range of validity is: [value-range .. value+range]
  If op is omitted we assume op to be == and if range is omitted
  it is assumed to be 0.
  See FuzzyComparator.Compare for information about the return value of
  comparisons.
  Example:
    fuzzy = FuzzyComparator("== 100 ~10")
    fuzzy.Compare(100) -> 1
    fuzzy.Compare(105) -> 0.5
    fuzzy.Compare(90)  -> 0
    fuzzy.Compare(89)  -> False
  """
  def __init__(self, input):
    if isinstance(input, str):
      m = re.match("([><=]=?)?[ ]*(\\-?[0-9]+\\.?[0-9]*)[ ]*" +
                   "(~[ ]*[0-9]+\\.?[0-9]*)?", input)

      if not m:
        raise ValueError("Malformed comparator: '" + input + "'")

      operator = m.group(1)
      value = m.group(2)
      fuzzyness = m.group(3)

      if value is None:
        raise ValueError("Malformed comparator: '" + input + "'")

      self._target = float(value)

      if operator is not None:
        self._operator_str = m.group(1)
      else:
        self._operator_str = "=="

      self._operator = FuzzyComparator._operator_mapping[self._operator_str]
      if fuzzyness is not None:
        self._fuzzyness = float(fuzzyness[1:])
      else:
        self._fuzzyness = 0
    else:
      self._target = float(input)
      self._fuzzyness = 0
      self._operator = FuzzyComparator._CompareEq
      self._operator_str = "=="

  def __str__(self):
    result = self._operator_str + "{0:.4g}".format(self._target)
    if self._fuzzyness > 0:
      result = result + "~{0:.4g}".format(self._fuzzyness)
    return result

  def Compare(self, value):
    """
    compares a number with the comparison string provided when constructing
    this object. The return value is:
      False: if the value is out of the valid range
      1: if the value is matched by the unequality/equality operation
      0..1: if the value is somewhere in the range of validity.
    """
    score = self._operator(self, value)
    if score < 0:
      return False
    elif score > 1:
      return 1
    else:
      return score

  def _CompareEq(self, value):
    if self._fuzzyness > 0:
      return (self._fuzzyness - math.fabs(self._target - value)) / \
           self._fuzzyness
    else:
      return value == self._target

  def _CompareGte(self, value):
    if self._fuzzyness > 0:
      return self._CompareGt(value)
    else:
      return value >= self._target

  def _CompareGt(self, value):
    if self._fuzzyness > 0:
      return float(value - self._target + self._fuzzyness) / self._fuzzyness
    else:
      return value > self._target

  def _CompareLte(self, value):
    if self._fuzzyness > 0:
      return self._CompareLt(value)
    else:
      return value <= self._target

  def _CompareLt(self, value):
    if self._fuzzyness > 0:
      return float(self._target - value + self._fuzzyness) / self._fuzzyness
    else:
      return value < self._target

  _operator_mapping = {
    "==": _CompareEq,
    "=" : _CompareEq,
    ">=": _CompareGte,
    ">" : _CompareGt,
    "<=": _CompareLte,
    "<" : _CompareLt,
  }
